////////////////////////////////////////////////////////////////////////////////
//
//  Pixelami.com
//  Copyright 2011 Original Authors (Alexander Syed et al)
//  All Rights Reserved.
//
//  NOTICE: Pixelami permits you to use, modify, and distribute this file
//  in accordance with the terms of the license agreement accompanying it.
//
////////////////////////////////////////////////////////////////////////////////

package org.pixelami.hercularity
{
	import flash.events.ErrorEvent;
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.events.IEventDispatcher;
	import flash.utils.Dictionary;
	
	import org.pixelami.hercularity.ab_internal;
	import org.pixelami.hercularity.conditions.Condition;
	import org.pixelami.hercularity.descriptors.Action;
	import org.pixelami.hercularity.events.StepErrorEvent;
	import org.pixelami.hercularity.events.StepEvent;
	
	[Event(name="stepComplete",type="org.pixelami.hercularity.events.StepEvent")]
	[Event(name="stepFailWarning",type="org.pixelami.hercularity.events.StepEvent")]
	[Event(name="stepError",type="org.pixelami.hercularity.events.StepErrorEvent")]
	
	[DefaultProperty("actions")]
	public class Step extends EventDispatcher
	{
		static private var instanceCount:uint = 0;
		
		private var name:String = "Step"+instanceCount++;
		
		private var _actions:Vector.<Action>;
		
		private var actionTargetLookup:Dictionary;
		
		public function Step(...actions)
		{
			super(null);
			
			init();
			
			for(var i:uint = 0; i < actions.length; i++)
			{
				if(actions[i] is Action)
				{
					addAction(actions[i]);
				}
			}
			
			
			//this.actions = actions;
		}
		
		protected function init():void
		{
			_actions = new Vector.<Action>();
			actionTargetLookup = new Dictionary();
			_failedStack = new Vector.<Action>();
			_completedStack = new Vector.<Action>();
		}

		public function get actions():Vector.<Action>
		{
			return _actions;
		}

		public function set actions(value:Vector.<Action>):void
		{
			if(_actions != value)
			{
				_actions = value;
				createActions();
			}
		}
		
		protected function createActions():void
		{
			for(var i:uint = 0; i < actions.length; i++)
			{
				if(actions[i] is Action)
				{
					addAction(actions[i]);
				}
			}
		}

		public function addAction(action:Action):void
		{
			actions.push(action);
			registerAction(action);
		}
		
		public function removeAction(action:Action):void
		{
			actions.splice(actions.indexOf(action),1);
			unregisterAction(action);
		}
		
		protected function registerAction(action:Action):void
		{
			actionTargetLookup[action.target] = action;
			//action.target.addEventListener(action.successEventType,OnActionComplete);
			//action.target.addEventListener(action.failureEventType,OnActionError);
		}
		
		protected function unregisterAction(action:Action):void
		{
			action.target.removeEventListener(action.successEventType,OnActionComplete);
			action.target.removeEventListener(action.failureEventType,OnActionError);
			delete actionTargetLookup[action.target];
		}
		
		/**
		 * All Action instances contained in <code> actions</code> are started. An Action is started by calling Action::startMethodName on Action::target.
		 * NOTE: All Actions are run in parallel. The Step is finished when either all actions have completed successfully 
		 * or any Action with <code>breakOnFailure</code> set to <code>true</code> dispatches an error 
		 * - at which time all pending actions events are ignored and the Step breaks.
		 * <br/><br/>
		 * To run Actions in Serial wrap them in a Step and chain Steps using e.g. <code>new Step().doStep(new Step());</code>
		 */
		public function run():void
		{
			// we need to makle a copy of actions array to handle the situation where a Task fires an event
			// immediatley thereby adjusting the length of the actions array during the loop
			var startActions:Vector.<Action> = actions.slice();
			for(var i:uint = 0; i < startActions.length; i++)
			{
				var action:Action = startActions[i];
				// In order to allow some Action wrapper Classes (see DeferredAction) to create targets lazily (defer creation until they are needed) 
				// we only want to trigger their creation and addListeners when absolutlely necessary.
				if(!action.target.hasEventListener(action.successEventType))
				{
					action.target.addEventListener(action.successEventType,OnActionComplete);
				}
				if(!action.target.hasEventListener(action.failureEventType))
				{
					action.target.addEventListener(action.failureEventType,OnActionError);
				}
				
				addEventListener(StepEvent.TASK_FAIL_WARNING,OnTaskFailWarning);
				
				action.target[action.runMethodName]();
			}
			
		}
		
		private var _doStep:Step;
		
		/**
		 * Allows a Step to be chained to this Step instance. The chained Step will be run if any <code>Condition::method</code>< returns <code>true</code>
		 * If no Condition has been defined via <code>thenIf()</code> then next Step is run automatically (provided the stack 
		 * has not aborted due to an Action failing and the Action's <code>breakOnFailure</code> was set to <code>true</code>).
		 */
		public function thenDo(value:Step):Step
		{
			_doStep = value;
			return this;
		}
		
		private var _otherwise:Step;
		public function otherwise(value:Step):Step
		{
			_otherwise = value;
			return this;
		}
		
		private var _thenIf:Condition;
		public function thenIf(value:Condition):Step
		{
			_thenIf = value;
			return this;
		}
		
		
		private var _failedStack:Vector.<Action>;
		private var _completedStack:Vector.<Action>;
		
		/**
		 * @return Returns a Vector of failed Actions in this Step only. Use getStackErrors() to get all failed Actions.
		 */
		
		public function get failedStack():Vector.<Action>
		{
			return _failedStack;
		}
		
		/**
		 * @return Returns a Vector of successfully completed Actions in this Step only.
		 */

		public function get completedStack():Vector.<Action>
		{
			return _completedStack;
		}

		
		
		protected function OnActionComplete(event:Event):void
		{
			var action:Action = actionTargetLookup[event.target];
			//trace("OnActionComplete: "+action);
			_completedStack.push(action);
			cleanupAction(action);
		}
		
		protected function OnActionError(event:Event):void
		{
			var action:Action = actionTargetLookup[event.target];
			//trace("OnActionError: "+action);
			
			
			// we can pass an error message up the stack . . . but we have no guarantee 
			// what property of the target represents and error.
			// to workaround this potential problem we store the original Error event in the Action
			// allowing the developer access to the original Error event instance and any pertinant info 
			// contained within it.
			action.ab_internal::errorEvent = event;
			
			// push the failed Action instance on the failedStack
			_failedStack.push(action);
			
			// check if we are supposed to bail on errors
			if(action.breakOnFailure)
			{
				//trace("breakOnFailure");
				fatalError = true;
				
				var se:StepErrorEvent = new StepErrorEvent(StepErrorEvent.STEP_ERROR,false,false);
				se.errorTarget = action;
				dispatchEvent(se);
				removeAction(action);
				// clear all other actions
				while(actions.length > 0) 
				{
					removeAction(actions[0]);
				}
			}
			else
			{
				trace("non critical error encountered");
				// we don't bail - nontheless - curious minds might want to know that an Action failed
				dispatchEvent(new StepEvent(StepEvent.TASK_FAIL_WARNING));
				//removeAction(action);
				cleanupAction(action);
			}
		}
		
		protected function OnTaskFailWarning(event:Event):void
		{
			removeEventListener(StepEvent.TASK_FAIL_WARNING,OnTaskFailWarning);
			dispatchEvent(event);
		}
		
		/**
		 * Post processing of a failed or completed Action
		 */
		
		protected function cleanupAction(action:Action):void
		{
			removeAction(action);
			//trace(actions.length);
			doNext();
		}
		
		private var fatalError:Boolean;
		private var conditionFail:Boolean;
		
		/**
		 * Called after each action completes or fails until all Actions have finished
		 * at which time <code>thenDo</code>, <code>thenIf</code> and <code>otherwise</code> are processed.
		 */
		
		protected function doNext():void
		{
			if(actions.length == 0)
			{
				if(_thenIf != null && !conditionFail)
				{
					// call the condition method within this scope.
					if(!_thenIf.method.apply(this,[this]))
					{
						conditionFail = true;
					}
					
					if(conditionFail)
					{
						// if the condition test failed then chack if we have an "otherwise" method defined
						// if we do then we will set it as a our next _doStep.
						if(_otherwise != null)
						{
							trace("doing otherwise");
							_doStep = _otherwise;
						}
						else
						{
							// otherwsie it's just an out and out Error - nothing more to do other than dispatch an Event 
							// and set fatalError flag.
							trace("fatalError");
							fatalError = true;
							dispatchEvent(new StepErrorEvent(StepErrorEvent.STEP_ERROR));
						}
					}
					
					
				}
				
				
				if(!fatalError)
				{
					if(_doStep != null)
					{
						trace("doing next step " +_doStep);
						_doStep.addEventListener(StepErrorEvent.STEP_ERROR,OnDoStepFailOrComplete);
						_doStep.addEventListener(StepEvent.STEP_COMPLETE,OnDoStepFailOrComplete);
						_doStep.run();
					}
					else
					{
						trace("step complete");
						dispatchEvent(new StepEvent(StepEvent.STEP_COMPLETE));
					}
				}
			}
		}
		
		/**
		 * Handles the outcome of the next Step in the stack
		 * re-dispatches the event allowing it "bubble" up the stack.
		 */
		
		protected function OnDoStepFailOrComplete(event:Event):void	
		{
			//trace(event);
			_doStep.removeEventListener(StepErrorEvent.STEP_ERROR,OnDoStepFailOrComplete);
			_doStep.removeEventListener(StepEvent.STEP_COMPLETE,OnDoStepFailOrComplete);
			
			dispatchEvent(event);
		}
		
		/**
		 * @return Returns the next Step in the stack
		 */
		
		public function getNextStep():Step
		{
			return _doStep;
		}
		
		/**
		 * Returns all steps in the stack
		 * Note: If this method is called after execution is complete and an "otherwise" was executed this will be returned instead of the "doStep".
		 * Otherwise it will only return a stack of "doStep"s and not the "otherwise" Steps
		 * 
		 * @return Returns Vector.<Step> of all steps
		 * 
		 */
		
		public function getStack():Vector.<Step>
		{
			var stack:Vector.<Step> = new Vector.<Step>();
			
			var _nextStep:Step = this;
			while(_nextStep)
			{
				stack.push(_nextStep);
				_nextStep = _nextStep.getNextStep();
			}
			trace("getStack() "+stack);
			return stack;
		}
		
		/**
		 * Collects all failed actions from the stack
		 * @return Returns Vector.<Action> of failed Actions
		 * 
		 */
		public function getErrorStack():Vector.<Action>
		{
			var fails:Vector.<Action> = new Vector.<Action>();
			var stack:Vector.<Step> = getStack();
			
			for each(var step:Step in stack)
			{
				//trace("step.failstack: "+step.failedStack);
				for each(var action:Action in step.failedStack)
				{
					//trace("action: "+action);
					fails.push(action);
				}
			}
			return fails;
		}
		
		/**
		 * @return Returns lengh of Step stack (after the this Step instance)
		 * 
		 */
		
		public function length():uint
		{
			var l:uint = 0;
			var n:Step = _doStep;
			while(n)
			{
				l++;
				n = n.getNextStep();
			}
			return l;
		}
		
		/**
		 * Add a step to the last (deepest) item in the stack
		 * @return Returns the new final Step
		 * 
		 */
		/*
		public function addStep(step:Step):Step
		{
			var n:Step = this;
			while(n)
			{
				n = n.getNextStep();
			}
			n.doStep(step);
			
			return this;
		}
		*/
		
		override public function toString():String
		{
			return "["+name+"]";
		}
	}
}